package com.opennews.openplatform

import groovy.io.FileType
import org.camunda.bpm.engine.*
import org.camunda.bpm.engine.form.FormField
import org.camunda.bpm.engine.history.HistoricProcessInstance
import org.camunda.bpm.engine.impl.persistence.entity.ExecutionEntity
import org.camunda.bpm.engine.impl.persistence.entity.ProcessDefinitionEntity
import org.camunda.bpm.engine.impl.persistence.entity.TaskEntity
import org.camunda.bpm.engine.migration.MigrationPlan
import org.camunda.bpm.engine.runtime.ActivityInstance
import org.camunda.bpm.engine.task.Comment
import org.grails.io.support.ClassPathResource

/**
 * Proxy class of Camunda workflow.
 *
 * Official documents:
 * https://camunda.com/products/bpmn-engine/
 * https://docs.camunda.org/manual/7.12/user-guide/process-engine/process-engine-api/?__hstc=252030934.6be3d702f098614fca7f8d07da4cd182.1586395716681.1586395716681.1586395716681.1&__hssc=252030934.3.1586395716682&__hsfp=2662054324
 */
class WorkflowProxy {
    ProcessEngine processEngine
    RepositoryService repositoryService
    RuntimeService runtimeService
    TaskService taskService
    FormService formService
    HistoryService historyService
    ManagementService managementService
    String processLocation

    private String longDateFormat = "yyyy-MM-dd HH:mm:ss"
    private String acceptedFileType = ".bpmn"

    /* Shared collect closures */
    /**
     * @return The list of Map: Key executionId, Value type String;
     */
    private executionCollectClosure = { ExecutionEntity execution -> [
        executionId: execution.id,
        activityId: execution.activityId,
        activityInstanceId: execution.activityInstanceId,
        businessKey: execution.businessKey,
        processDefinitionId: execution.processDefinitionId,
        ended: execution.ended,
        suspended: execution.suspended
    ]}

    /**
     * @return The list of Map: Key activityId, Value type String;
     *                         Key activityName, Value type String;
     *                         Key activityType, Value type String;
     *                         Key executionIds, Value type Array;
     */
    private activityInstanceCollectClosure = { ActivityInstance activity -> [
        activityId: activity.activityId,
        activityName: activity.activityName,
        activityType: activity.activityType,
        executionIds: activity.executionIds
    ]}

    /**
     * @return The list of Map: Key id, Value type String;
     *                          Key executionId, Value type String;
     *                          Key name, Value type String;
     *                          Key taskDefinitionKey, Value type String;
     *                          Key assignee, Value type String;
     *                          Key owner, Value type String;
     *                          Key createTime, Value type String;
     *                          Key suspended, Value type Boolean;
     */
    private taskCollectClosure = { TaskEntity task -> [
        id: task.id,
        executionId: task.executionId,
        name: task.name,
        taskDefinitionKey: task.taskDefinitionKey,
        assignee: task.assignee,
        owner: task.owner,
        createTime: task.createTime.format(longDateFormat),
        suspended: task.suspended
    ]}

    /**
     * @return The list of map: Key id, Value type String;
     *              Key label, Value type String;
     *              Key typeName, Value type String;
     *              Key value, Value type String;
     *              Key validationConstraints, Value type Map;
     *                  Key name, Value type String;
     *                  Key configuration, value type Object;
     */
    private formFieldsCollectClosure = { FormField field -> [
        id: field.id,
        label: field.label,
        typeName: field.typeName,
        value: field.value.value,
        validationConstraints: field.validationConstraints.collect { validation -> [
            name: validation.name,
            configuration: validation.configuration
        ]}
    ]}

    /**
     * @return The list of Map: Key id, Value type String;
     *                          Key fullMessage, value type String;
     *                          Key time, value type String;
     */
    private taskCommentCollectClosure = { Comment comment -> [
        id: comment.id,
        fullMessage: comment.fullMessage,
        time: comment.time.format(longDateFormat)
    ]}

    /**
     * @return The list of map: Key id, Value type String;
     *              Key key, Value type String;
     *              Key name, Value type String;
     *              Key version, Value type String;
     */
    private processDefinitionCollectClosure = { ProcessDefinitionEntity definition -> [
        id: definition.id,
        key: definition.key,
        name: definition.name,
        version: definition.version
    ]}

    /**
     * @return The list of Map: Key instanceId, Value type String;
     *                          Key processDefinitionKey, Value type String;
     *                          Key businessKey, Value type String;
     *                          Key startTime, Value type String;
     *                          Key endTime, Value type String;
     *                          Key activities, Value type List of Map;
     *                              Key activityType, Value type String;
     *                              Key activityId, Value type String.
     *                              Key name, Value type String.
     *                              Key startTime, Value type String.
     *                              Key endTime, Value type String.
     *                              Key assignee, Value type String.
     *                          Key variables, Value type List of Map;
     *                              Key name, Value type String;
     *                              Key value, Value type Object;
     */
    private historicProcessInstanceCollectClosure = { HistoricProcessInstance instance -> [
        instanceId: instance.id,
        processDefinitionId: instance.processDefinitionId,
        processDefinitionKey: instance.processDefinitionKey,
        businessKey: instance.businessKey,
        startTime: instance.startTime != null ? instance.startTime.format(longDateFormat) : null,
        endTime: instance.endTime != null ? instance.endTime.format(longDateFormat) : null,
        state: instance.state,
        deleteReason: instance.deleteReason,
        activities: historyService.createHistoricActivityInstanceQuery().processInstanceId(instance.id.toString()).orderPartiallyByOccurrence().asc().list().collect { activity -> [
            activityType: activity.activityType,
            activityId: activity.activityId,
            name: activity.activityName,
            startTime: activity.startTime != null ? activity.startTime.format(longDateFormat) : null,
            endTime: activity.endTime != null ? activity.endTime.format(longDateFormat) : null,
            assignee: activity.assignee
        ]},
        variables: historyService.createHistoricVariableInstanceQuery().processInstanceId(instance.id).list().collect { variable -> [
            name: variable.name,
            value: variable.value
        ]}
    ]}
    /* End of Shared collect closures */

    // region: Process Definition
    /**
     * Gets all latest active process definitions
     * @return map: Elements reference to processDefinitionCollectClosure.
     */
    List<Map<String, ?>> getAllLatestActiveProcessDefinitions() {
        return repositoryService.createProcessDefinitionQuery().active().latestVersion().list().collect(processDefinitionCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Gets all latest suspended process definitions
     * @return map: Elements reference to processDefinitionCollectClosure.
     */
    List<Map<String, ?>> getAllLatestSuspendedProcessDefinitions() {
        return repositoryService.createProcessDefinitionQuery().suspended().latestVersion().list().collect(processDefinitionCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Gets the process definition by id (NOT the id property on the modeler designer, genereated by engine.)
     * @param processDefinitionId: The string of the process definition id.
     * @return map: Elements reference to processDefinitionCollectClosure.
     */
    Map<String, ?> getProcessDefinitionById(String processDefinitionId) {
        def result = repositoryService.getProcessDefinition(processDefinitionId).collect(processDefinitionCollectClosure)

        return getFirstElement(result)
    }

    /**
     * Gets the latest version process definition by key (This is the id property on the modeler designer)
     * @param processDefinitionKey: The string of the process definition key.
     * @return map: Elements reference to processDefinitionCollectClosure.
     */
    Map<String, ?> getLatestProcessDefinitionByKey(String processDefinitionKey) {
        def result = repositoryService.createProcessDefinitionQuery().latestVersion().processDefinitionKey(processDefinitionKey).singleResult().collect(processDefinitionCollectClosure)

        return getFirstElement(result)
    }

    /**
     * Gets the process definition by key (This is the id property on the modeler designer)
     * @param processDefinitionKey: The string of the process definition key.
     * @return map: Elements reference to processDefinitionCollectClosure.
     */
    List<Map<String, ?>> getProcessDefinitionByKey(String processDefinitionKey) {
        return repositoryService.createProcessDefinitionQuery().processDefinitionKey(processDefinitionKey).orderByProcessDefinitionVersion().desc().list().collect(processDefinitionCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Gives access to a deployed process model, e.g., a BPMN 2.0 XML file, through a stream of bytes.
     * @param processDefinitionKey: The string of the process definition key.
     * @return map: Key contentType, Value type String;
     *              Key fileStream, Value type InputStream;
     */
    Map<String, ?> getProcessDefinitionModelByKey(String processDefinitionKey) {
        // Option 1, retrieves the bpmn file via the file io and the resource name.
        //    new File(processDefinitionList[0].resourceName).getText("UTF-8")

        // Option 2, gets the bpmn file stream via repositoryService.getProcessModel without creating instance of File class.

        // Goes to option 2.
        def result = getLatestProcessDefinitionByKey(processDefinitionKey)

        return result == null ? null : getProcessModel(result.id.toString())
    }

    /**
     * Gives access to a deployed process model, e.g., a BPMN 2.0 XML file, through a stream of bytes.
     * @param processDefinitionId: Id of a ProcessDefinition, cannot be null.
     */
    Map<String, ?> getProcessModel(String processDefinitionId) {
        // Option 1, retrieves the bpmn file via the file io and the resource name.
        //    new File(processDefinitionList[0].resourceName).getText("UTF-8")

        // Option 2, gets the bpmn file stream via repositoryService.getProcessModel without creating instance of File class.

        // Goes to option 2.
        return [
            contentType: "application/xml",
            fileStream: repositoryService.getProcessModel(processDefinitionId)
        ]
    }

    /**
     * Suspends the all process definitions with the given key (= id in the bpmn20.xml file).
     * @param processDefinitionKey: The string of the process definition key.
     * @return boolean: True means successful. False means failed.
     */
    void suspendProcessDefinitionByKey(String processDefinitionKey) {
        repositoryService.suspendProcessDefinitionByKey(processDefinitionKey)
    }

    /**
     * Suspends the all process definitions with the given key (= id in the bpmn20.xml file).
     * @param processDefinitionKey: The string of the process definition key.
     * @param suspendProcessInstances: If true, all the process instances of the provided process definition will be suspended too.
     * @param suspensionDate:  The date on which the process definition will be suspended. If null, the process definition is suspended immediately. Note: The job executor needs to be active to use this!
     * @return boolean: True means successful. False means failed.
     */
    void suspendProcessDefinitionByKey(String processDefinitionKey, Boolean suspendProcessInstances, Date suspensionDate) {
        repositoryService.suspendProcessDefinitionByKey(processDefinitionKey, suspendProcessInstances, suspensionDate)
    }

    /**
     * Activates the process definition with the given key (=id in the bpmn20.xml file).
     * @param processDefinitionKey: The string of the process definition key.
     * @return boolean: True means successful. False means failed.
     */
    void activateProcessDefinitionByKey(String processDefinitionKey) {
        repositoryService.activateProcessDefinitionByKey(processDefinitionKey)
    }

    /**
     * Activates the process definition with the given key (=id in the bpmn20.xml file).
     * @param processDefinitionKey: The string of the process definition key.
     * @param activateProcessInstances: If true, all the process instances of the provided process definition will be activated too.
     * @param activationDate: The date on which the process definition will be activated. If null, the process definition is suspended immediately. Note: The job executor needs to be active to use this!
     * @return boolean: True means successful. False means failed.
     */
    void activateProcessDefinitionByKey(String processDefinitionKey, Boolean activateProcessInstances, Date activationDate) {
        repositoryService.activateProcessDefinitionByKey(processDefinitionKey, activateProcessInstances, activationDate)
    }

    /**
     * Deploys one process file by specific file name. By now supports bpmn file format only.
     * The bpmn file must be located in grails-app/conf/processes directory. No sub-directory supported.
     * @param filename The String of the filename. For example, MyExample.bpmn
     */
    void deploy(String filename) {
        // Checks the file extension. By now only bpmn format supported.
        if (!filename.endsWith(acceptedFileType)) {
            throw new IllegalArgumentException("ONLY supports bmpn file type")
        }

        // Gets the ClassPathResource with the pre-configured directory and provided filename.
        def resource = new ClassPathResource("${processLocation}/${filename}")

        // Deploys this bpmn file.
        deploy(resource.file)
    }

    /**
     * Deploys one process file by specific File instance. By now supports bpmn file format only.
     * The bpmn file must be located in the pre-configured directory.
     * The default is grails-app/conf/processes directory. No sub-directory supported.
     * Try to make some changes in bpmn file if the deployment does not happen without error.
     * @param file The File instance. For example, MyExample.bpmn
     */
    void deploy(File file) {
        // Checks the file extension. By now only bpmn format supported.
        if (!file.name.endsWith(acceptedFileType)) {
            throw new IllegalArgumentException("ONLY supports bmpn file type")
        }

        String resourceName

        try {
            resourceName = file.absolutePath
        } catch (IOException e) {
            resourceName = file.name
        }

        // Deploys this bpmn file with deployment builder.
        def deploymentBuilder = repositoryService.createDeployment().enableDuplicateFiltering(true).name("GrailsHotDeployment")
        deploymentBuilder.addInputStream(resourceName, file.newInputStream())
        deploymentBuilder.deploy()
    }

    /**
     * Deploys all bmpn files located in pre-configured directory.
     * The default is grails-app/conf/processes directory. No sub-directory supported.
     * Try to make some changes in bpmn file if the deployment does not happen without error.
     */
    void deploy() {
        // Gets all files in grails-app/conf/processes directory.
        new ClassPathResource("${processLocation}").file.eachFile(FileType.FILES) { file ->
            // Checks the file extension. By now only bpmn format supported.
            if (file.name.endsWith(acceptedFileType)) {
                // Deploys this bmpn file.
                deploy(file)
            }
        }
    }

    /**
     * Deletes the process definition which belongs to the given process definition id.
     * @param processDefinitionId The String id of process definition.
     */
    void deleteProcessDefinition(String processDefinitionId) {
        repositoryService.deleteProcessDefinition(processDefinitionId)
    }
    // endregion

    // region: Activity
    /**
     * Allows retrieving the activity instance tree for a given process instance.
     * @param processInstanceId: The string of process instance id.
     * @return map: Elements reference to activityInstanceCollectClosure.
     */
    Map<String, ?> getActivityInstance(String processInstanceId) {
        def result = runtimeService.getActivityInstance(processInstanceId).collect(activityInstanceCollectClosure)

        return getFirstElement(result)
    }
    // endregion

    // region: Process Instance
    /**
     * Deletes an existing runtime process instance.
     * @param processInstanceId: Id of process instance to delete, cannot be null.
     * @param deleteReason: Reason for deleting, which will be stored in the history. Can be null.
     */
    void deleteProcessInstance(String processInstanceId, String deleteReason) {
        runtimeService.deleteProcessInstance(processInstanceId, deleteReason)
    }

    /**
     * Activates the process instance with the given id.
     * If you have a process instance hierarchy, activating one process instance from the hierarchy will not
     * activate other process instances from that hierarchy.
     * @param processInstanceId: Id of process instance to delete, cannot be null.
     * @return boolean: True means successful. False means failed.
     */
    void activateProcessInstanceById(String processInstanceId) {
        runtimeService.activateProcessInstanceById(processInstanceId)
    }

    /**
     * Suspends the process instance with the given id. This means that the execution is stopped,
     * so the token state will not change. However, actions that do not change token state,
     * like setting/removing variables, etc. will succeed.
     * @param processInstanceId: Id of process instance to delete, cannot be null.
     * @return boolean: True means successful. False means failed.
     */
    void suspendProcessInstanceById(String processInstanceId) {
        runtimeService.suspendProcessInstanceById(processInstanceId)
    }

    /**
     * Gets all active process instances
     * @return map: Elements reference to executionCollectClosure.
     */
    List<Map<String, ?>> getAllProcessInstances() {
        return runtimeService.createProcessInstanceQuery().active().list().collect(executionCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Gets the active process instance with the id
     * @param processInstanceId: The string of process instance id.
     * @return map: Elements reference to executionCollectClosure.
     */
    Map<String, ?> getProcessInstance(String processInstanceId) {
        def result = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).active().list().collect(executionCollectClosure)

        return getFirstElement(result)
    }

    /**
     * Queries the related active process instances with the given process definition key (=id in the bpmn20.xml file).
     * @param processDefinitionKey: The string of the process definition key.
     * @return map: Elements reference to executionCollectClosure.
     */
    List<Map<String, ?>> queryProcessInstance(String processDefinitionKey) {
        return runtimeService.createProcessInstanceQuery().processDefinitionKey(processDefinitionKey).active().list().collect(executionCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Starts a new process instance in the latest version of the process definition with the given key.
     * @param processDefinitionKey: Key of process definition, cannot be null.
     * @param processInstanceVariables: The variables to pass, can be null.
     * @return map: Elements reference to executionCollectClosure.
     */
    Map<String, ?> startProcessInstanceByKey(String processDefinitionKey, Map<String, ?> processInstanceVariables) {
        return startProcessInstanceByKey(processDefinitionKey, null, processInstanceVariables)
    }

    /**
     * Starts a new process instance in the latest version of the process definition with the given key.
     * @param processDefinitionKey: Key of process definition, cannot be null.
     * @param businessKey: A key that uniquely identifies the process instance in the context of the given process definition.
     * @param processInstanceVariables: The variables to pass, can be null.
     * @return map: Elements reference to executionCollectClosure.
     */
    Map<String, ?> startProcessInstanceByKey(String processDefinitionKey, String businessKey, Map<String, ?> processInstanceVariables) {
        def result

        if (businessKey == null || businessKey == "") {
            result = runtimeService.startProcessInstanceByKey(processDefinitionKey, processInstanceVariables).executionEntity.collect(executionCollectClosure)
        } else {
            result = runtimeService.startProcessInstanceByKey(processDefinitionKey, businessKey, processInstanceVariables).executionEntity.collect(executionCollectClosure)
        }

        return getFirstElement(result)
    }

    /**
     * Queries all the related historic process instances with given process definition key (=id in the bpmn20.xml file).
     * @param processDefinitionKey: The string of the process definition key.
     * @return map: Elements reference to historicProcessInstanceCollectClosure.
     */
    List<Map<String, ?>> historicProcessInstanceQuery(String processDefinitionKey) {
        return historyService.createHistoricProcessInstanceQuery().processDefinitionKey(processDefinitionKey).orderByProcessInstanceStartTime().desc().list().collect(historicProcessInstanceCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Only queries process instances which had a global variable with the given value when they ended.
     * @param name: The variable name.
     * @param value: The variable value.
     * @return map: Elements reference to historicProcessInstanceCollectClosure.
     */
    List<Map<String, ?>> historicProcessInstanceQueryByVariableValueEquals(String name, Object value) {
        return historyService.createHistoricProcessInstanceQuery().variableValueEquals(name, value).orderByProcessInstanceStartTime().desc().list().collect(historicProcessInstanceCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Queries historic process instance data with given process instance id.
     * @param processInstanceId: The string of the process instance id.
     * @return map: Elements reference to historicProcessInstanceCollectClosure.
     */
    Map<String, ?> getHistoricProcessInstance(String processInstanceId) {
        def result = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).list().collect(historicProcessInstanceCollectClosure)

        return getFirstElement(result)
    }

    /**
     * Deletes historic process instance. All historic activities, historic task and historic
     * details (variable updates, form properties) are deleted as well.
     * @param processInstanceId: The string of process instance id.
     */
    void deleteHistoricProcessInstance(String processInstanceId) {
        historyService.deleteHistoricProcessInstance(processInstanceId)
    }
    // endregion

    // region: Execution
    /**
     * Queries all the realted executions with given process instance id.
     * @param processInstanceId: The string of the process instance id.
     * @return map: Elements reference to executionCollectClosure.
     */
    List<Map<String, ?>> queryProcessExecution(String processInstanceId) {
        return runtimeService.createExecutionQuery().processInstanceId(processInstanceId).list().collect(executionCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Gets all variable values that are defined in the execution scope, without taking outer scopes into account.
     * @param executionId: Id of execution, cannot be null.
     * @param isLocal: Boolean, if it is true then will not look in outer scopes.
     * @return
     */
    Map<String, ?> getExecutionVariables(String executionId, boolean isLocal) {
        return isLocal ? runtimeService.getVariablesLocal(executionId) : runtimeService.getVariables(executionId)
    }

    /**
     * Updates or creates given variables for an execution (including parent scopes).
     * If the variables are not already existing, they will be created in the process instance (which is the root execution).
     * @param executionId: The string of the execution id.
     * @param variables: Map containing name (key) and value of variables, can be null.
     * @param isLocal: Boolean, if it is true, the variables will be created in the given execution instead of process instance.
     */
    void setExecutionVariables(String executionId, Map<String, ?> variables, boolean isLocal) {
        isLocal ? runtimeService.setVariablesLocal(executionId, variables) : runtimeService.setVariables(executionId, variables)
    }
    // endregion

    // region: Task
    /**
     * Queries the related tasks with given process instance id.
     * @param processInstanceId: The string of the process instance id.
     * @return map: Elements reference to taskCollectClosure.
     */
    List<Map<String, ?>> queryTask(String processInstanceId) {
        return taskService.createTaskQuery().processInstanceId(processInstanceId).list().collect(taskCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Gets the task with given task id.
     * @param taskId: The string of task id.
     * @return map: Elements reference to taskCollectClosure.
     */
    Map<String, ?> getTask(String taskId) {
        def result = taskService.createTaskQuery().taskId(taskId).list().collect(taskCollectClosure)

        return getFirstElement(result)
    }

    /**
     * Gets the task belongs to particular process instance and the one who is the assignee
     * @param processInstanceId: The string of the process instance
     * @param taskAssignee: The string of the task assignee identity. normally, it's the user id.
     * @return map: Elements reference to taskCollectClosure.     *
     */
    Map<String, ?> getTaskByProcessInstanceAndAssignee(String processInstanceId, String taskAssignee) {
        def result = taskService.createTaskQuery().processInstanceId(processInstanceId).taskAssignee(taskAssignee).list().collect(taskCollectClosure)

        return getFirstElement(result)
    }

    /**
     * Only queries tasks which had a global variable with the given value.
     * @param name: The variable name.
     * @param value: The variable value.
     * @return map: Elements reference to taskCollectClosure.
     */
    Map<String, ?> getTaskByProcessVariableValueEquals(String name, Object value) {
        def result = taskService.createTaskQuery().processVariableValueEquals(name, value).list().collect(taskCollectClosure)

        return getFirstElement(result)
    }

    /**
     * Gets the task with the given assignee
     * @param taskAssignee: The string of the task assignee identity. normally, it's the user id.
     * @return map: Elements reference to taskCollectClosure.
     */
    List<Map<String, ?>> getTaskByAssignee(String taskAssignee) {
        return taskService.createTaskQuery().taskAssignee(taskAssignee).list().collect(taskCollectClosure) as List<Map<String, ?>>
    }

    /**
     * Creates a comment to a task and/or process instance.
     * @param taskId: The string of task id.
     * @param processInstanceId: The string of the process instance.
     * @param message: The string of the message content.
     */
    void createTaskComment(String taskId, String processInstanceId, String message) {
        taskService.createComment(taskId, processInstanceId, message)
    }

    /**
     * Gets all the comments attached to the specific task.
     * @param taskId: The string of the task id.
     * @return map: Elements reference to taskCommentCollectClosure.
     */
    Map<String, ?> getTaskComments(String taskId) {
        return taskService.getTaskComments(taskId).collect(taskCommentCollectClosure) as Map<String, ?>
    }

    /**
     * Gets all the comments attached to the specific task.
     * @param taskId: The string of the task id.
     * @return commentId: The string of the comment id.
     */
    Map<String, ?> getTaskComment(String taskId, String commentId) {
        def result = taskService.getTaskComment(taskId, commentId).collect(taskCommentCollectClosure)

        return getFirstElement(result)
    }

    /**
     * Gets all variables and search in the task scope and if available also the execution scopes.
     * @param taskId: The string of the task id.
     * @param isLocal: Boolean, if it is true search only in the task scope.
     * @return The map instance.
     */
    Map<String, ?> getTaskVariables(String taskId, Boolean isLocal) {
        return isLocal ? taskService.getVariablesLocal(taskId) : taskService.getVariables(taskId)
    }

    /**
     * Sets variables on a task. If the variable is not already existing, it will be created in the most outer scope.
     * This means the process instance in case this task is related to an execution.
     * @param taskId: The string of the task id.
     * @param variables: The map type variable.
     * @param isLocal: Boolean, if it is true it will be created in the task only.
     */
    void setTaskVariables(String taskId, Map<String, ?> variables, Boolean isLocal) {
        isLocal ? taskService.setVariablesLocal(taskId, variables) : taskService.setVariables(taskId, variables)
    }

    /**
     * Retrieves a list of all variables for rendering a start from. The method takes into account
     * FormData specified for the start event. This allows defining default values for form fields.
     * @param processDefinitionId: The string of the process definition id.
     * @return The map instance.
     */
    Map<String, ?> getStartFormVariables(String processDefinitionId) {
        return formService.getStartFormVariables(processDefinitionId)
    }

    /**
     * Retrieves a list of all variables for rendering a start from. The method takes into account
     * FormData specified for the start event. This allows defining default values for form fields.
     * @param processDefinitionId: The string of the process definition id.
     * @return The map instance.
     */
    Map<String, ?> getStartFormDataFormFields(String processDefinitionId) {
        return formService.getStartFormData(processDefinitionId).formFields.collect(formFieldsCollectClosure) as Map<String, ?>
    }

    /**
     * Retrieves a list of all variables for rendering a task form. In addition to the task variables
     * and process variables, the method takes into account FormData specified for the task.
     * This allows defining default values for form fields.
     *
     * The example of input & output parameter:
     * User Task A -> User Task B
     * Task A contains a input and a output parameter.
     * getTaskFormVariables executes on User Task A would return its input parameter before it completes.
     * After User Task A completes, getTaskFormVariables executes on User Task B would return A's output parameter before B completes.
     *
     * @param taskId: The string of task id.
     * @return The map instance.
     */
    Map<String, ?> getTaskFormVariables(String taskId) {
        return formService.getTaskFormVariables(taskId)
    }

    /**
     * Retrieves all data necessary for rendering a form to complete a task.
     * This can be used to perform rendering of the forms outside of the process engine.
     * @param taskId: The string of task id.
     * @return map: Elements reference to formFieldsCollectClosure.
     */
    Map<String, ?> getTaskFormDataFormFields(String taskId) {
        return formService.getTaskFormData(taskId).formFields.collect(formFieldsCollectClosure) as Map<String, ?>
    }

    /**
     * Changes the assignee of the given task to the given userId.
     * @param taskId: The string of the task id.
     * @param userId: The string of the user id.
     */
    void setTaskAssignee(String taskId, String userId) {
        taskService.setAssignee(taskId, userId)
    }

    /**
     * Transfers ownership of this task to another user.
     * @param taskId: The string of the task id.
     * @param userId: The string of the user id.
     */
    void setTaskOwner(String taskId, String userId) {
        taskService.setOwner(taskId, userId)
    }

    /**
     * Completes the task with given task id.
     * @param taskId: The string of task id.
     * @param processInstanceVariables: The map type variable for the process instance.
     * @param taskVariables: The map type variable for the task.
     * @param taskVariablesLocal: The map type variable for the task locally.
     */
    void completeTask(String taskId,
                      Map<String, ?> processInstanceVariables,
                      Map<String, ?> taskVariables,
                      Map<String, ?> taskVariablesLocal) {
        def task = getTask(taskId)

        if (task) {
            runtimeService.setVariables(task.executionId.toString(), processInstanceVariables)
            taskService.setVariables(taskId, taskVariables)
            taskService.setVariablesLocal(taskId, taskVariablesLocal)
            taskService.complete(taskId)
        }
    }

    /**
     * Completes the task with given task id.
     * @param taskId: The string of task id.
     * @param processInstanceVariables: The map type variable for the process instance.
     * @param taskVariables: The map type variable for the task.
     */
    void completeTask(String taskId, Map<String, ?> processInstanceVariables, Map<String, ?> taskVariables) {
        completeTask(taskId, processInstanceVariables, taskVariables, null)
    }

    /**
     * Completes the task with given task id.
     * @param taskId: The string of task id.
     * @param processInstanceVariables: The map type variable for the process instance.
     */
    void completeTask(String taskId, Map<String, ?> processInstanceVariables) {
        completeTask(taskId, processInstanceVariables, null, null)
    }

    /**
     * Completes the task with given task id.
     * @param taskId: The string of task id.
     */
    void completeTask(String taskId) {
        completeTask(taskId, null, null, null)
    }
    // endregion

    // region: Process Instance Migration
    /**
     * Gets a built migration plan with given sourceProcessDefinitionId and targetProcessDefinitionId.
     * @param sourceProcessDefinitionId: The process definition that instances are migrated from.
     * @param targetProcessDefinitionId: The process definition that instances are migrated to.
     * @param mappedActivities: The list of map, contains the activity id of the source and target.
     *                                           Key sourceActivityId, Value type String;
     *                                           Key targetActivityId, Value type String;
     * @return: The instance of MigrationPlan.
     */
    MigrationPlan getMigrationPlan(String sourceProcessDefinitionId, String targetProcessDefinitionId, List<Map<String, String>> mappedActivities) {
        // Creates a migration plan to migrate process instance between different process definitions.
        // Returns a fluent builder that can be used to specify migration instructions and build the plan.
        def migrationPlan = runtimeService.createMigrationPlan(sourceProcessDefinitionId, targetProcessDefinitionId)

        // Attachs each the mapped activity
        mappedActivities.each {
            // Adds a migration instruction that maps activity instances of the source activity
            // (of the source process definition) to activity instances of the target activity (of the target process definition)
            migrationPlan.mapActivities(it.sourceActivityId, it.targetActivityId)
        }

        // Returns the instance of MigrationPlan.
        return migrationPlan.build()
    }

    /**
     * Migrates all the process instances with given sourceProcessDefinitionId and targetProcessDefinitionId.
     * @param sourceProcessDefinitionId: The process definition that instances are migrated from.
     * @param targetProcessDefinitionId: The process definition that instances are migrated to.
     * @param mappedActivities: The list of map, contains the activity id of the source and target.
     *                                           Key sourceActivityId, Value type String;
     *                                           Key targetActivityId, Value type String;
     * @param processInstanceIds: The list of the String of process instance id.
     */
    void migrateProcessInstances(String sourceProcessDefinitionId, String targetProcessDefinitionId, List<Map<String, String>> mappedActivities, List<String> processInstanceIds) {
        // Gets the migration plan
        def migrationPlan = getMigrationPlan(sourceProcessDefinitionId, targetProcessDefinitionId, mappedActivities)

        // Executes the migration plan
        runtimeService.newMigration(migrationPlan).processInstanceIds(processInstanceIds).execute()
    }

    /**
     * Migrates all the process instances with given sourceProcessDefinitionId and targetProcessDefinitionId.
     * @param sourceProcessDefinitionId: The process definition that instances are migrated from.
     * @param targetProcessDefinitionId: The process definition that instances are migrated to.
     * @param mappedActivities: The list of map, contains the activity id of the source and target.
     *                                           Key sourceActivityId, Value type String;
     *                                           Key targetActivityId, Value type String;
     */
    void migrateAllProcessInstances(String sourceProcessDefinitionId, String targetProcessDefinitionId, List<Map<String, String>> mappedActivities) {
        // Gets the migration plan
        def migrationPlan = getMigrationPlan(sourceProcessDefinitionId, targetProcessDefinitionId, mappedActivities)

        // Creates the process instance query to get all the process instances of the source process definition.
        def processInstanceQuery = runtimeService.createProcessInstanceQuery().processDefinitionId(migrationPlan.getSourceProcessDefinitionId())

        // Executes the migration plan
        runtimeService.newMigration(migrationPlan).processInstanceQuery(processInstanceQuery).execute()
    }
    // endregion

    // region: Private Methods
    private Map<String, ?> getFirstElement(List container) {
        return container.size() == 0 ? null : container.first() as Map<String, ?>
    }
    // endregion
}
